# photoviewer.py
import numpy as N
# pysage import
from pysage import ActorManager, Actor, get_logger
# pil imports 
import Image
# wx imports
import wx
from wx.lib.floatcanvas.Utilities import BBox
import wx.lib.floatcanvas.FloatCanvas as fc
# python built imports
import string
import cStringIO as StringIO
# user imports
import util
from messages import *

omanager = ActorManager.get_singleton()
logger = get_logger()

shadow_cache = {}

SPEED = 1000

class ThumbnailCanvas(fc.FloatCanvas):
    def zoom(self, scale, pos):
        # before we zoom, record the world position of the mouse click, that world position should not change after we zoom
        old_world = self.PixelToWorld((pos.x, pos.y))
        
        # then we zoom
        # self.Scale = self.Scale*factor
        self.Scale = scale
        self.SetToNewScale(False)
        
        # then adjust the new mouse pixel position translated world position back to old world position
        new_world = self.PixelToWorld((pos.x, pos.y))
        delta = util.vec2(new_world[0] - old_world[0], new_world[1] - old_world[1]) * -1.0
        self.MoveImage((delta.x, delta.y), 'World')
    def ZoomToBB(self, NewBB=None, DrawFlag=True):
        """

        Zooms the image to the bounding box given, or to the bounding
        box of all the objects on the canvas, if none is given.

        """
        
        if NewBB is not None:
            BoundingBox = NewBB
        else:
            if self.BoundingBoxDirty:
                self._ResetBoundingBox()
            BoundingBox = self.BoundingBox
        if BoundingBox is not None:
            self.ViewPortCenter = N.array(((BoundingBox[0,0]+BoundingBox[1,0])/2,
                                         (BoundingBox[0,1]+BoundingBox[1,1])/2 ),N.float_)
            self.MapProjectionVector = self.ProjectionFun(self.ViewPortCenter)
            # Compute the new Scale
            BoundingBox = BoundingBox*self.MapProjectionVector # this does need to make a copy!
            try:
                self.Scale = min(abs(self.PanelSize[0] / (BoundingBox[1,0]-BoundingBox[0,0])),
                                 abs(self.PanelSize[1] / (BoundingBox[1,1]-BoundingBox[0,1])) )
            except ZeroDivisionError: # this will happen if the BB has zero width or height
                try: #width == 0
                    self.Scale = (self.PanelSize[0]  / (BoundingBox[1,0]-BoundingBox[0,0]))
                except ZeroDivisionError:
                    try: # height == 0
                        self.Scale = (self.PanelSize[1]  / (BoundingBox[1,1]-BoundingBox[0,1]))
                    except ZeroDivisionError: #zero size! (must be a single point)
                        self.Scale = 1

            if DrawFlag:
                self._BackgroundDirty = True
        else:
            # Reset the shifting and scaling to defaults when there is no BB
            self.ViewPortCenter= N.array( (0,0), N.float)
            self.Scale= 1
        self.SetToNewScale(DrawFlag=DrawFlag)

class PhotoViewer(ThumbnailCanvas, Actor):
    '''simple panel that will serve as our viewer component'''
    subscriptions = ['AlbumSelectedMessage', 'MoveThumbnailWindowMessage', 'GrantFocusToViewerMessage', 'ZoomViewerMessage',
                     'UserClickMessage', 'ImageResourceUpdatedMessage', 'RemoveAlbumMessage', 'FolderNotAdded']
    def __init__(self, parent):
        fc.FloatCanvas.__init__(self, parent, -1, BackgroundColor = 'Light Blue')
        Actor.__init__(self)
        
        self.initialize_navigation()
        
        self._loc_cache = {}
        self.images = {}
        self._time_last_update = None
        self.has_yielded = False
        
        # flag to indicate if we've looked at the pics for reloading
        self._check_for_reload = False
        self.bitmaps = {}
        
        self._is_zoomed_in = False
        self._saved_viewport = None
        
        # bounding box
        self.bbox = None
        self.last_tick_time = None
    def image_mouse_collide(self, pt):
        for imageid in self.images:
            # image = util.draw_location(self._loc_cache[imageid], self.loc_offset, self.scale), util.BASE_THUMB_SIZE * self.scale
            image = self.images[imageid][1]
            ptbox = BBox.asBBox((self.PixelToWorld((pt.x, pt.y )),self.PixelToWorld((pt.x, pt.y))))
            if util.collide(image.BoundingBox, ptbox):
                return imageid
        return None
    def initialize_navigation(self):
        self.scale = 1.0
        self.origin_offset = None
        self.loc_offset = util.vec2(0,0)
    def get_visible_images(self):
        return set(_id for _id, obj in self.images.items() if util.collide(obj[1].BoundingBox, self.ViewPortBB))
    def get_invisible_images(self):
        return set(self.images.keys()) - set(self.get_visible_images())
    def handle_AlbumSelectedMessage(self, msg):
        '''populates the cache and stores the albumid'''
        # reinit float canvas
        self.InitAll()
        # reinit navigation
        self.initialize_navigation()
        # self.RemoveObjects(self.images.values())
        self.images = {}
        self.prepare_album(msg.get_property('albumid'))
        # self.update_drawing()
    def handle_FolderNotAdded(self, msg):
        dlg = wx.MessageDialog(self, 'Folder "%s" could not be added' % msg.get_property('path'),
                               'Message',
                               wx.OK | wx.ICON_INFORMATION
                               #wx.YES_NO | wx.NO_DEFAULT | wx.CANCEL | wx.ICON_INFORMATION
                               )
        dlg.ShowModal()
        dlg.Destroy()  
        return True
    def prepare_album(self, albumid):
        '''called when first initializing some cache for an album like: image locations, etc'''
        self._albumid = albumid
        hsize, vsize = util.get_image_block_size(len(omanager.find('mapi').get_album_photos(self._albumid)))
        for i,p in enumerate(omanager.find('mapi').get_album_photos(self._albumid)):
            loc = util.get_image_location(i, hsize, vsize, util.BASE_THUMB_SIZE.x, util.BASE_THUMB_SIZE.y, 10, 20, 15)
            self._loc_cache[p] = util.vec2(loc.x, -loc.y)
    def handle_RemoveAlbumMessage(self, msg):
        if msg.get_property('albumid') == self._albumid:
            self.ClearAll()
            self.Draw()
        return False
    def handle_ImageResourceUpdatedMessage(self, msg):
        '''cache the resource loaded from "resource loading" group'''
        # only yield once
        if not self.has_yielded:
            wx.GetApp().Yield(True)
        photoid = msg.get_property('image')
        data = msg.get_property('data')
        img = wx.ImageFromStream(StringIO.StringIO(data))
        back = util.back_drop(img, cache=shadow_cache)
        # self._image_cache[photoid] = img, 
        # size = util.get_size_from_scale(util.vec2(*self._image_cache[photoid][0].GetSize()), self.scale)
        # image = util.string_to_image(msg.get_property('data'), msg.get_property('size'))
        self.refresh_image(photoid, img, back)
        # logger.info('image "%s": received new resolution of "%s"' % (photoid, msg.get_property('size')))
        return True
    def handle_MoveThumbnailWindowMessage(self, msg):
        # immediately stop any reloading of image resources
        omanager.abort_message('ReloadImageResourceMessage')
#        displacement = msg.get_property('displacement')
        # self.ScrollWindow(displacement.x, displacement.y)
        # scale dwarfs the movement
        self.loc_offset += msg.get_property('displacement')
        d = msg.get_property('displacement') * -1.0
        # self.update_drawing()
        self.MoveImage((d.x, d.y), 'Pixel')
        
        # sets images dirty
        self._check_for_reload = True
        # record the time
        self._time_last_update = util.get_time()
        
        return True
    def handle_ZoomViewerMessage(self, msg):
        # immediately stop any reloading of image resources
        omanager.abort_message('ReloadImageResourceMessage')
        # save the old scale
        old_scale = self.scale 

        # calculating new scale
        scale = msg.get_property('rotation') / 3000.0
        self.scale *= (1 + scale)
        
        if self.scale < 0.3:
            self.scale = 0.3
        elif self.scale > 1000:
            self.scale = 1000.0
            
        self.zoom(self.scale, msg.get_property('position'))
        
        # sets images dirty
        self._check_for_reload = True
        # record the time
        self._time_last_update = util.get_time()
        
#        omanager.abort_message('ReloadImageResourceMessage')
#        omanager.queue_message(ReloadImageResourceMessage(scale=new_scale))
        return True
    def reload_image(self, imageid, scale):
        '''if imageid is cached and the requested resolution is large enough, then pull from cache
            otherwise, tell the resource group to load it
        '''
        image, back = self.images.get(imageid, None)
        if image:
            old_size = util.vec2(image.bmpWidth, image.bmpHeight)
            new_size = util.trim_size(util.get_size_from_scale(old_size, scale), util.MAX_RENDER_SIZE)
            if new_size.x <= old_size.x:
                logger.info('image %s: not reloading new: %s, old: %s scale= %s' % (imageid, new_size, old_size, scale))
                # self.refresh_image(imageid)
            else:
                logger.info('image %s: trying to reload due to size  new: %s, old: %s' % (imageid, new_size, old_size))
                omanager.queue_message_to_group('resource_loading', ReloadImageResourceMessage(size=new_size, imageid=imageid, enforce_size=1))
                # omanager.queue_message(RefreshImageMessage(imageid = imageid, size = new_size))
        else:
            new_size = util.BASE_THUMB_SIZE * self.scale
            logger.info('image %s: reloading due to cache miss of size "%s"' % (imageid, new_size))
            omanager.queue_message_to_group('resource_loading', ReloadImageResourceMessage(size=new_size, imageid=imageid, enforce_size=0))
    def refresh_image(self, imageid, image, back):
        '''refresh the screen displaying this image'''
        # image = util.pil_to_image(util.max_size(self._image_cache.get(imageid), size))
        # image, back = self._image_cache.get(imageid)
        if not self.origin_offset:
            self.origin_offset = util.vec2(*self.PixelToWorld((0,0)))
        
        if imageid in omanager.find('mapi').get_album_photos(self._albumid):
            # image = self._image_cache[photoid]
            # loc = util.draw_location(self._loc_cache[photoid], self.loc_offset, self.scale)
            loc = self._loc_cache[imageid] + self.origin_offset
            # if we already have this image, remove it first
            if imageid in self.images:
                self.RemoveObject(self.images[imageid][0])
                self.RemoveObject(self.images[imageid][1])
            self.images[imageid] = (self.AddScaledBitmap(image, (loc.x + 9, loc.y - 9), Height = util.BASE_THUMB_SIZE.y, Position = "tl", InForeground = True),
                                    self.AddScaledBitmap(back, (loc.x,loc.y), Height = util.BASE_THUMB_SIZE.y + 17, Position = "tl"))
        self.Draw()
        return True
    def handle_GrantFocusToViewerMessage(self, msg):
        self.SetFocus()
        return True
    def handle_UserClickMessage(self, msg):
        pos = msg.get_property('position')
        logger.debug('colliding image "%s"' % self.image_mouse_collide(pos))
        if self._is_zoomed_in:
            self._is_zoomed_in = False
            self.bbox = self._saved_viewport
            self._is_inside = BBox.asBBox(self.ViewPortBB).Inside(self.bbox)
            # self.scale = self.Scale
        else:
            imageid = self.image_mouse_collide(pos)
            if imageid:
                self._is_zoomed_in = True
                self._saved_viewport = self.ViewPortBB
#                self.ZoomToBB(self.images[imageid].BoundingBox)
#                self.scale = self.Scale
                self.bbox = self.images[imageid][1].BoundingBox # + ((-3,-3), (4,4))
                self._is_inside = BBox.asBBox(self.ViewPortBB).Inside(self.bbox)
                # figure out the final scale here
                # eventual scale is window size / world size of the picture bbox frame
                self.reload_image(imageid, float(self.Size[0]) / float(self.bbox[1][0] - self.bbox[0][0]))
        return True
    def update(self, evt=None):
        current_time = util.get_time()
        if not self.last_tick_time:
            self.last_tick_time = current_time
        elapsed = current_time - self.last_tick_time
        self.last_tick_time = current_time
        
        # increate the idle time here if the GUI starts to be irresponsive
        if self._time_last_update and self._check_for_reload and util.get_time() - self._time_last_update > .33:
            logger.debug('photoviewer: the user is starting to idle, %s pics visible' % len(self.get_visible_images()))
            # omanager.queue_message_to_group('resource_loading', ReloadImageResourceMessage(scale=self.scale, visible_images=self.get_visible_images()))
            map(lambda x: self.reload_image(x, self.scale), self.get_visible_images())
            self._check_for_reload = False
            
        
#        # if i have a box that i need to move to, then step towards it
#        if not self.bbox == None:
#            # if (self.ViewPortBB[1,0] - self.ViewPortBB[0,0]) > (self.bbox[1,0] - self.bbox[0,0] + 15.0):
#            if abs((self.ViewPortBB[1,0] - self.ViewPortBB[0,0]) - (self.bbox[1,0] - self.bbox[0,0])) > 5:
#                logger.info('viewport: %s, pic: %s, viewport inside pic %s' % (BBox.asBBox(self.ViewPortBB), self.bbox, BBox.asBBox(self.ViewPortBB).Inside(self.bbox)))
#                current_bb = self.ViewPortBB
#                d_min = (self.bbox[0] - self.ViewPortBB[0]) * .2
#                d_max = (self.bbox[1] - self.ViewPortBB[1]) * .2
#                new_min = self.ViewPortBB[0] + d_min
#                new_max = self.ViewPortBB[1] + d_max
#                
#                new_bb = BBox.BBox((new_min, new_max))
#                
#                self.ZoomToBB(new_bb)
#                self.scale = self.Scale
#            else:
#                self.bbox = None
                
        # if i have a box that i need to move to, then step towards it
        if not self.bbox == None:
            # if (self.ViewPortBB[1,0] - self.ViewPortBB[0,0]) > (self.bbox[1,0] - self.bbox[0,0] + 15.0):
            # if abs((self.ViewPortBB[1,0] - self.ViewPortBB[0,0]) - (self.bbox[1,0] - self.bbox[0,0])) > 15:
            logger.debug('viewport: %s, pic: %s, viewport inside pic %s' % (BBox.asBBox(self.ViewPortBB), self.bbox, BBox.asBBox(self.ViewPortBB).Inside(self.bbox)))
            if self._is_inside == BBox.asBBox(self.ViewPortBB).Inside(self.bbox):
                current_bb = self.ViewPortBB
                d_min = (self.bbox[0] - self.ViewPortBB[0]) 
                d_max = (self.bbox[1] - self.ViewPortBB[1])
                
                mag = util.magnitude(d_min)
                
                ud_min = d_min / mag
                ud_max = d_max / mag
                
                delta = .5 * elapsed * SPEED
                
                # if overshot, then zoom to place
                if util.magnitude(ud_min * delta) > util.magnitude(d_min) or util.magnitude(ud_max * delta) > util.magnitude(d_max):
                    new_bb = self.bbox
                    self.bbox = None
                else:
                    new_min = self.ViewPortBB[0] + ud_min * delta
                    new_max = self.ViewPortBB[1] + ud_max * delta
                    new_bb = BBox.BBox((new_min, new_max))
                
                self.ZoomToBB(new_bb)
                self.scale = self.Scale
            else:
                self.bbox = None
                

#USE_BUFFERED_DC = True
#
#class PhotoViewer(wx.ScrolledWindow, Actor):
#    '''simple panel that will serve as our viewer component'''
#    subscriptions = ['AlbumSelectedMessage', 'MoveThumbnailWindowMessage', 'GrantFocusToViewerMessage', 'ZoomViewerMessage',
#                     'UserClickMessage', 'ImageResourceUpdatedMessage']
#    def __init__(self, parent):
#        wx.ScrolledWindow.__init__(self, parent, -1)
#        Actor.__init__(self)
#        self.Bind(wx.EVT_PAINT,self.OnPaint)
#        self.Bind(wx.EVT_SIZE, self.OnSize)
#        self._path = ''
#        self._albumid = None
#        self._loc_cache = {}
#        
#        # this stores the left top corner of the viewport in image space
#        self.loc_offset = util.vec2(0,0)
#        self.scale = 1.0
#        self._time_last_rendered = None
#        
#        self._time_last_zoom = None
#        
##        self.SetVirtualSize((2000,2000))
#        # self.BackgroundColour = wx.WHITE
#        self.OnSize(None)
#    def handle_ZoomViewerMessage(self, msg):
#        # record the time
#        self._time_last_zoom = util.get_time()
#        # save the old scale
#        old_scale = self.scale 
#
#        # calculating new scale
#        scale = msg.get_property('rotation') / 3000.0
#        self.scale *= (1 + scale)
#        
#        if self.scale < 0.2:
#            self.scale = 0.2
#        elif self.scale > 1000:
#            self.scale = 1000.0
#            
#        new_scale = self.scale 
#        
#        # recompute space origin using the focus and the new scale
#        self.loc_offset = util.get_new_viewport(msg.get_property('position'), old_scale, self.loc_offset, new_scale)
#            
#        # omanager.abort_message('ReloadImageResourceMessage')
#        # omanager.queue_message(ReloadImageResourceMessage(scale=new_scale))
#        self.update_drawing()
#        return True
#    def user_ready_for_update(self):
#        '''if it's been more than .1 of a second since last time we zoomed 
#        '''
#        if not self._time_last_zoom:
#            return False
#        return util.get_time() - self._time_last_zoom > .1
#    def prepare_album(self, albumid):
#        '''called when first initializing some cache for an album like: image locations, etc'''
#        self._albumid = albumid
#        hsize, vsize = util.get_image_block_size(len(omanager.find('mapi').get_album_photos(self._albumid)))
#        for i,p in enumerate(omanager.find('mapi').get_album_photos(self._albumid)):
#            loc = util.get_image_location(i, hsize, vsize, util.BASE_THUMB_SIZE.x, util.BASE_THUMB_SIZE.y, 10, 20, 20)
#            self._loc_cache[p] = util.vec2(loc.x, -loc.y)
#    def image_visible(self, imageid):
#        image = util.draw_location(self._loc_cache[imageid], self.loc_offset, self.scale), util.BASE_THUMB_SIZE * self.scale
#        viewport = util.vec2(0,0), util.vec2(*self.GetClientSizeTuple())
#        return util.collide(image, viewport)
#    def image_mouse_collide(self, pt):
#        for imageid in self._loc_cache:
#            image = util.draw_location(self._loc_cache[imageid], self.loc_offset, self.scale), util.BASE_THUMB_SIZE * self.scale
#            if util.collide(image, (pt, util.vec2(0,0))):
#                return imageid
#        return None
#    def handle_ImageResourceUpdatedMessage(self, msg):
#        self.update_drawing()
#        return True
#    def update(self, evt=None):
#        # if last time we zoomed was later than last time we rendered, and current time is more than a second away
#        # we will update the resources
##        if self._time_last_zoom and self._time_last_rendered:
##            print self._time_last_zoom > self._time_last_rendered, util.get_time() > self._time_last_zoom + 1.0
##            if self._time_last_zoom > self._time_last_rendered and util.get_time() > self._time_last_zoom + 1.0:
##                omanager.queue_message(ReloadImageResourceMessage())
##        return True
#        # self.update_drawing()
#        return True
#    def handle_UserClickMessage(self, msg):
#        imageid = self.image_mouse_collide(msg.get_property('position'))
#        if imageid:
#            # make a menu
#            menu = wx.Menu()
#            # Show how to put an icon in the menu
##            item = wx.MenuItem(menu, self.popupID1,"One")
##            bmp = images.getSmilesBitmap()
##            item.SetBitmap(bmp)
##            menu.AppendItem(item)
#            # add some other items
#            menu.Append(-1, "Comment")
#            # make a submenu
##            sm = wx.Menu()
##            sm.Append(self.popupID8, "sub item 1")
##            sm.Append(self.popupID9, "sub item 1")
##            menu.AppendMenu(self.popupID7, "Test Submenu", sm)
#
#
#            # Popup the menu.  If an item is selected then its handler
#            # will be called before PopupMenu returns.
#            self.PopupMenu(menu)
#            menu.Destroy()
#        return True
#    def handle_GrantFocusToViewerMessage(self, msg):
#        self.SetFocus()
#        return True
#    def handle_MoveThumbnailWindowMessage(self, msg):
#        # scale dwarfs the movement
#        self.loc_offset += msg.get_property('displacement')
#        
#        displacement = msg.get_property('displacement')
#        self.ScrollWindow(displacement.x, displacement.y)
#        # self.update_drawing()
#        return True
#    def handle_AlbumSelectedMessage(self, msg):
#        '''populates the cache and stores the albumid'''
#        self.prepare_album(msg.get_property('albumid'))
#        # self.update_drawing()
#        return True
#    def OnSize(self, evt):
#        size = self.GetClientSizeTuple()
#        self._buffer = wx.EmptyBitmap(*size)
#        # self.update_drawing()
#    def update_drawing(self):
#        '''use this for rendering to screen that is outside of the onsize events
#            this draws to the buffer
#        '''
#        if self._time_last_rendered:
#            if util.get_time() - self._time_last_rendered < .03:
#                return
#            
#        # logger.debug('photoviewer: rendering: %s' % self._time_last_rendered)
#        
#        if USE_BUFFERED_DC:
#            dc = wx.BufferedDC(wx.ClientDC(self), self._buffer)
#            self.draw(dc)
#            
#        self._time_last_rendered = util.get_time()
#    def draw(self, dc):
#        dc.SetBackground(wx.Brush('Light Blue'))
#        dc.Clear()
#        # dc.SetUserScale(self.scale, self.scale)
#        try:
#            gc = wx.GraphicsContext.Create(dc)
#        except NotImplementedError:
#            dc.DrawText("This build of wxPython does not support the wx.GraphicsContext "
#                        "family of classes.",
#                        25, 25)
#            return
#        font = wx.SystemSettings.GetFont(wx.SYS_DEFAULT_GUI_FONT)
#        font.SetWeight(wx.BOLD)
#        gc.SetFont(font)
#
#        # draw the pictures if we have an album
#        if self._albumid:
#            for photoid in omanager.find('mapi').get_album_photos(self._albumid):
#                # culling
#                # only worry about this if this is visible
#                if not self.image_visible(photoid):
#                    continue
#                bmp, scale = omanager.find('resourcecontroller').get_bitmap(photoid)                
#                if not bmp:
#                    continue
#                gc.PushState()             # save current translation/scale/other state 
#                # this is coordinate in image space
#                loc = util.draw_location(self._loc_cache[photoid], self.loc_offset, self.scale)
#                # translate it by the viewport_origin
#                gc.Translate(loc.x, loc.y)
#                gc.Scale(self.scale / scale, self.scale / scale)
#                
#                bsz = bmp.GetSize()
#                
#                # gc.SetPen(wx.Pen("navy", 1))
#                # gc.SetBrush(wx.Brush("pink"))
#                # rsize = util.vec2(15,15) * self.scale
#                # gc.DrawRectangle(0,0, rsize.x, rsize.y)
#                
#                gc.DrawBitmap(bmp,
#                              #-bsz.width, 
#                              #-bsz.height/2,
#        
#                              # -bsz.width/2.5, 
#                              # -bsz.height/2.5,
#                              0,0,
#                              bsz.width, bsz.height)
#        
#                gc.PopState()
#    def OnPaint(self, evt):
#        '''event handler upon which a paing device context can be created and used'''
#        if USE_BUFFERED_DC:
#            # this just blits the content of the bitmap buffer to screen, very fast
#            dc = wx.BufferedPaintDC(self, self._buffer)
#        else:
#            dc = wx.PaintDC(self)
#            dc.DrawBitmap(self._buffer, 0, 0)













        
        
        